perm filename MOVIN.LSP[F87,JMC] blob sn#850852 filedate 1987-12-28 generic text, type T, neo UTF8
;;; -*- Syntax: Common-lisp; Package: PZ; Default-character-style: (:FIX :BOLD :NORMAL) -*-


;;; SOLVE is the main function - given a board, it sets up the problem for solution and then
;;; solves it.   The file "Puzzle:puzzle;Calling-dependencies.text" shows the way all of the
;;; subsidiary functions are reached.

(defun solve (board &key (show-interval 1000))
  (initialize-problem board)
  (showboard board)
  (do ()
      ((goalp *base-board*)
       (format t "~&~74t~'b⊂SOLVED!~⊃~&")
       (showboard *base-board*))
    (next-node *base-board* :show-interval show-interval)))

;;; INITIALIZE-PROBLEM resets all the statistical counters, clears the various memories
;;; inside the boards, discovers if the board is already partially solved.  It copies the
;;; problem onto the *base-board*.  Because of this copying, the problem state inside the
;;; board that is being solved is not touched - so that when, say, *easy-puzzle* is solved,
;;; its original state is unchanged and it can be solved again without re-evaluating the
;;; Defparameter statement that set it up.

(defun initialize-problem (board)
  (setq *nodes-considered* 0
	*rejections* 0
	*acceptances* 0)
  (clear-heuristic-statistics)
  (setf (board-moves board) nil)
  (setf (board-blank-origin board)(board-blank board))
  (evaluate-initial-position board)
  (check-goodness board)
  (setf *original-board* board)
  (copy-board-position *base-board* *original-board*)
  (copy-board-position *hidden-board* *original-board*)	; Notice partial solutions.
  (setf (fifo-queue-line *queue*)(list nil))
  )

;;; Each Node is a list of moves going back to the original position.  The Queue will never
;;; be empty.  When a move is accepted, the queue is flushed, but then the current position
;;; is added to the queue.  When the current position is the original position, the queue =
;;; '(nil).  The last number in the list is the current position of the blank.  (In the case
;;; of the NIL node, the original blank position is stored on the board.)  STORED-SUCCESSORS
;;; filters the move that undoes the last move.

;;; The Tforms are trace hooks.  When turned on, they print out information about each
;;; expanded node.  When turned off, they just emit a characteristic beep that allows a
;;; rough audio trace of the solution.

(defun next-node (BaseBoard &key (show-interval 1000))
  (let ((node (next *queue*)))
    (loop for child in (stored-successors node BaseBoard)
	  for thischild = (cons child node)
	  do (new-position-for-board
	       thischild *hidden-board*)
	     (incf *nodes-considered*)
	     (when (and show-interval
			(zerop (mod  *nodes-considered* show-interval)))
	       (showboard baseboard))
	     (cond ((may-reject (worse *hidden-board* BaseBoard)))
		   ;; Don't pursue a worse position
		   ((may-accept (better *hidden-board* BaseBoard) thischild baseboard)
		    ;; Don't even look at any remaining children if this one is better than the 
		    ;; BoardPosition.
		    (return nil))
		   (t (Tform  2000 10000 ".")
		      (add thischild *queue*)))
	     )))

(defun may-reject (reason)
  (when reason
    (Tform 4500 20000 "..rejected by ~a" reason)
    (incf *rejections*)
    (incf (get reason :success))
    reason))

(defun may-accept (reason thischild baseboard)
  (when reason
    (Tform 900 0 "~&Considering move ~s" thischild)
    (accept thischild Baseboard)
    (unless (equal (coerce (board-position baseboard) 'list)
		   (coerce (board-position *hidden-board*) 'list))
      (format t "Didn't correctly reset board.~&")
      (showboard baseboard)(showboard *hidden-board*)
      (error "Lose, Lose.."))
    (evaluate-intermediate-position BaseBoard)
    (Tform 900 30000 " ..accepted by ~a." reason)
    (incf *acceptances*)
    (incf (get reason :success))
    (flush *queue*)
    (add thischild *queue*)
    (when (and *acceptance-trace*
	       (yes-or-no-p "Show Board? "))
      (showboard baseboard))
    reason
    ))

;;; MOVE executes a single move, by interchanging the blank and a tile.  Remember the
;;; current contents of the square into which the board is moving.  Set that square blank.
;;; Write the old contents of that square into the square's old position.  Note the new
;;; position of the blank.  Notice that this is a pure interchange operation.  No check for
;;; the legality of the move is performed.  The caller has responsibility for adding the
;;; move to the boards MOVES list.

(defun move (singlemove board)
  (let ((current-contents (aref (board-position board) (1- singlemove))))	
    (setf (aref (board-position board) (1- singlemove))	
	  :blank)
    (setf (aref (board-position board) (1- (board-blank board)))
	  current-contents)
    (setf (board-blank board) singlemove)
    ))


;;; We repeatedly have the following situation: we have a list of moves: 
;;; (Final ....Intermediate .... Initial) such that if the moves were executed in reverse
;;; order on the initial board, it would go through the intermediate state to the final
;;; position.  But the board is already in the intermediate state, and its MOVES list is a
;;; pointer to the sublist (Intermediate .... Initial).   We only want to execute the moves
;;; from Final back to Intermediate, in reverse order.  We do this by passing 
;;; (Final ....Intermediate .... Initial) as the longlist, the MOVES list of the board as
;;; the shortlist, recursing down longlist til we find shortlist, and then executing the
;;; moves as we come back up the stack.

(defun make-intervening-moves (longlist shortlist board)
  (cond ((and (null longlist)
	      (or (board-moves board) shortlist))
	 (showboard board)
	 (error "Never found a common sublist in Make-Intervening-Moves."))
	((not (eq longlist shortlist))
	 (make-intervening-moves (cdr longlist)
				 shortlist board)
	 (move (first longlist) board)
	 (setf (board-moves board) longlist))
	(t nil)))


;;; COMMON-ROOT is for moving the hidden board from a position corresponding to one node to
;;; a postion corresponding to the next node on the queue.  We want to back the hidden board
;;; back up to an common ancestor of the two nodes Because we are making a depth-first
;;; search, we know that the length of the two descents can be off by at most one link.

(defun common-root (seq1 seq2)
  (cond ((or (null seq1)(null seq2))
	 nil)
	((eq seq1 seq2) seq1)
	((eq (cdr seq1) seq2) seq2)
	(t (common-root (cdr seq1)(cdr seq2)))))

;;; Make the initial evaluation of the board.  Notice if it is already partially solved.

(defun evaluate-initial-position (board)
  (completed-chain board)
  ;; Figure out in which row the chain terminates.
  (setf (board-last-complete-row board)
	(floor (board-completed-chain board) (board-side board))))

;;; Calculate the length of the completed chain of tiles.  This will NOT notice if the chain
;;; has, say, been completed up through tile 12, but has been rotated out of final position
;;; - it only counts the tiles currently in place.

(defun completed-chain (board)
  ;; Go through the board position square by sqare, finding out how many tiles 
  ;; are already in the completed chain.  Leave the loop when you reach the end.
  (setf (board-completed-chain board) 0)
  (loop for place from 1 to (board-size board)
	if (equal place (position-contents place board))	
	  ;; Have to use EQUAL rather than = cuz of :blank.
	  do (setf (board-completed-chain board) place)	
	else
	  return place				; Final return for solved board.
	finally (return place)))

;;; A node (which represents a board position and is represented by a
;;; list of moves) is tested on the hidden-board.  If it is BETTER than
;;; the baseboard, the move is ACCEPTed, and the base board is advanced
;;; to that position by executing all of them moves in the node that go
;;; beyond the moves that the board has already executed.

(defun accept (movelist baseboard)
  (make-intervening-moves
    movelist (board-moves  baseboard) baseboard))

;;; Right now, intermediate positions are evaluated just the same way as are intial
;;; positions.


(defun evaluate-intermediate-position (board)
  (evaluate-initial-position board))

;;; NEW-POSITION-FOR-BOARD moves the hiddenboard between a position from
;;; one node to the next.

(defun new-position-for-board (newpos hiddenboard)
  (let ((commonroot (common-root newpos (board-moves hiddenboard))))
    (back-board-up-to-position (board-moves hiddenboard)
			       commonroot hiddenboard)
    (make-intervening-moves
      newpos commonroot hiddenboard)))


;;; The *hidden-board* must be backed up to the common ancestor of the
;;; destination board and its current board

(defun back-board-up-to-position (movelist commonroot board)
  (unless (eq movelist commonroot)
    (move (previous-move movelist board)
	  board)
    (pop (board-moves board))
    (back-board-up-to-position
      (cdr movelist) commonroot board)))

;;; Normally, the move that generated a particular position is just the second move on the
;;; board's list.  However, after the first move we cannot tell what move generated the
;;; current position by looking at the movelist.  Instead, the info is stored on the
;;; BLANK-ORIGIN slot of the board.

(defun previous-move (movelist board)
  (or (second movelist)
      (board-blank-origin board)))